package com.naico.core.service;

import com.naico.core.pojo.*;
import com.naico.util.FinancialUtility;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

/**
 * User: Dawood Ajaz
 * Date: Dec 27, 2008
 * Time: 10:20:45 PM
 */
public class RollDownPaymentService implements CalculatorService {

    // calculated roll down amount 
    private double rollDownAmount;
    // debt over payment
    private double overPayment;
    // last debt minimum payment
    private double lastDebtPayment;
    // what month debt pay-off
    private Date payOffMonth;
    // what month debt will include minimum payment of last debt
    private Date rollDownMonth;
    // flag to identify, when rolldown payment to be calculated
    private boolean rollDown = false;
    // flag for first debt will be having additional rolldown payment
    private boolean firstDebt = true;


    public RollDownPaymentService() {

    }

    public PaymentPlan generate(DebtInformationRequest debtInformationRequest) {

        //DebtInformationResult debtInfoResult = new DebtInformationResult();
        RollDownPaymentPlan rollDownPaymentPlan = new RollDownPaymentPlan();
        List<DebtInformation> debtInfoList = debtInformationRequest.getDebtInformationList();

        double totalMinimumPayment = debtInformationRequest.getTotalMinimumPayment();
        double totalActualPayment = debtInformationRequest.getTotalActualPayment();
        double extraPayment = debtInformationRequest.getExtraPayment();

        // setting totals
        rollDownPaymentPlan.setTotalMonthlyPayment(totalMinimumPayment + extraPayment);
        rollDownPaymentPlan.setTotalDebtAmount(debtInformationRequest.getTotalBalance());
        rollDownPaymentPlan.setTotalInterest(debtInformationRequest.getAverageInterest());

        // calculate rolldown amount
        double accumulatedRollDownAmount =
                computeAdditionalRollDownAmount(totalMinimumPayment,
                        totalActualPayment,
                        extraPayment);

        // re-order/sort the input debt information based on the debt order.
        //TODO: needs to see the design issue here and it must adaptable with new changes/ new debt orders.
        //TODO: debt order must be inputed from out of the box.
        debtInformationRequest.sortedDebtInformationList();

        for (DebtInformation debtInfo : debtInfoList) {
            setRollDownAmount(accumulatedRollDownAmount);
            if (getOverPayment() > 0) {
                setRollDownAmount(getRollDownAmount() + getOverPayment());
                //setRollDownAmount(getOverPayment());
            }

            // generate payment schedule
            PaymentSchedule paymentSchedule = generateSchedule(debtInfo);
            rollDownPaymentPlan.add(paymentSchedule);

            // if first debt has been processed,
            // then last debt payment will be sum up of
            // (minimum payment + accumulatedRollDownAmount + existing last debt payment)
            if (firstDebt) {
                // set processed debt minimum payment
                setLastDebtPayment(getLastDebtPayment() + debtInfo.getMinimumPayment() + accumulatedRollDownAmount);
            } else {
                // set processed debt minimum payment
                setLastDebtPayment(getLastDebtPayment() + debtInfo.getMinimumPayment());
            }

            // set roll down to false.
            setRollDown(false);
            // set first debt to false.
            setFirstDebt(false);
        }

        return rollDownPaymentPlan;
    }

    private PaymentSchedule generateSchedule(DebtInformation debtInformation) {

        PaymentSchedule schedule = new PaymentSchedule();
        List<PaymentScheduleItem> items = new ArrayList<PaymentScheduleItem>();

        String debtName = debtInformation.getDebtName();
        double balance = debtInformation.getBalance();
        double interestRate = debtInformation.getInterestRate();

        double payment;
        if (isRollDown()) {
            payment = debtInformation.getMinimumPayment() + getRollDownAmount();
        } else {
            payment = debtInformation.getMinimumPayment();
        }

        // compute first month payment
        PaymentScheduleItem firstMonthPayment = computeFirstMonthPayment(debtInformation, getRollDownAmount());
        // add first month payment
        items.add(firstMonthPayment);

        // compute interest
        double interest = FinancialUtility.computeMonthlyInterest(balance, interestRate);
        // calculate ending balance
        double endingBalance = FinancialUtility.computeEndingBalance(balance, payment, interest);

        // compute remaining months payment
        List<PaymentScheduleItem> remainingMonthsPayment =
                computeRemainingMonthsPayment(debtName, balance, payment,
                        endingBalance, interestRate, debtInformation.getStartDate());
        items.addAll(remainingMonthsPayment);
        schedule.setPaymentScheduleItems(items);

        return schedule;
    }

    private PaymentScheduleItem computeFirstMonthPayment(DebtInformation debtInformation, double additionalRollDownAmount) {
        // first rollDownSchedule item.
        PaymentScheduleItem firstMonthPayment = new PaymentScheduleItem();
        firstMonthPayment.setMonth(debtInformation.getStartDate());
        firstMonthPayment.setCreditorName(debtInformation.getDebtName());
        firstMonthPayment.setBeginningBalance(debtInformation.getBalance());
        double interest = FinancialUtility.computeMonthlyInterest(debtInformation.getBalance(), debtInformation.getInterestRate());
        firstMonthPayment.setInterest(interest);

        double payment;
        if (isFirstDebt()) {
            // set payment.
            payment = debtInformation.getMinimumPayment() + additionalRollDownAmount;
        } else {
            payment = debtInformation.getMinimumPayment();
        }

        // set payment.
        firstMonthPayment.setPayment(payment);

        // compute ending balance.
        double endingBalance =
                FinancialUtility.computeEndingBalance(debtInformation.getBalance(), payment, interest);

        firstMonthPayment.setEndingBalance(endingBalance);
        firstMonthPayment.setRollOver(isRollDown());
        return firstMonthPayment;
    }

    private List<PaymentScheduleItem> computeRemainingMonthsPayment(String debtName,
                                                                    double balance,
                                                                    double actualPayment,
                                                                    double endingBalance,
                                                                    double interestRate,
                                                                    Date startDate) {

        List<PaymentScheduleItem> remainingMonthsPayment = new ArrayList<PaymentScheduleItem>();
        // set the date.
        Calendar runningMonth = Calendar.getInstance();
        runningMonth.setTime(startDate);

        double originalActualPayment = actualPayment;
        int i = 0;
        double overPayment;
        // while balance become zero or 600 months i.e. 50 years
        while (endingBalance > 0) {

            // increase month by 1
            runningMonth.add(Calendar.MONTH, 1);

            // apply additional rolldown amount, in the same month when last debt is payoff
            if (getPayOffMonth() != null && getPayOffMonth().equals(runningMonth.getTime())) {
                // set roll down to true.
                setRollDown(true);
                actualPayment += getOverPayment();
                // reverse the over payment for next debt
                setOverPayment(0d);
                // set pay off month to null for next debt
                setPayOffMonth(null);
            }

            // addition of last debt minimum payment into actual payment
            if (getRollDownMonth() != null && getRollDownMonth().equals(runningMonth.getTime())) {
                actualPayment = originalActualPayment + getLastDebtPayment();
                // to keep rolldown month as running month so last debt minimum payment will be add into actual payment
                setRollDownMonth(runningMonth.getTime());
            }

            balance = endingBalance;
            double interest = FinancialUtility.computeMonthlyInterest(balance, interestRate);
            endingBalance = FinancialUtility.computeEndingBalance(balance, actualPayment, interest);

            // if ending balance goes below zero that means we are over paying
            double paymentAmount = actualPayment;
            if (endingBalance < 0) {
                overPayment = -(endingBalance);
                paymentAmount -= overPayment;
                endingBalance = 0.0;
                // set debt pay-off month
                setPayOffMonth(runningMonth.getTime());

                Calendar rollDownMonthCalc = Calendar.getInstance();
                rollDownMonthCalc.setTime(runningMonth.getTime());
                rollDownMonthCalc.add(Calendar.MONTH, 1);
                // set the roll down month.
                setRollDownMonth(rollDownMonthCalc.getTime());
                // set over payment
                setOverPayment(overPayment);
            }

            PaymentScheduleItem item = new PaymentScheduleItem();
            item.setMonth(runningMonth.getTime());
            item.setCreditorName(debtName);
            item.setBeginningBalance(balance);
            item.setInterest(interest);
            item.setPayment(paymentAmount);
            item.setEndingBalance(endingBalance);
            item.setRollOver(isRollDown());

            remainingMonthsPayment.add(item);

            //break the loop if 600 months i.e. 50 years are passed
            if (i++ >= 600)
                break;
        }

        // set remaining months payment
        return remainingMonthsPayment;
    }

    public static double computeAdditionalRollDownAmount(double totalMinimumPayment,
                                                         double totalActualPayment,
                                                         double additionalRollDownAmount) {
        return (totalActualPayment - totalMinimumPayment + additionalRollDownAmount);
    }

    public double getRollDownAmount() {
        return rollDownAmount;
    }

    public void setRollDownAmount(double rollDownAmount) {
        this.rollDownAmount = rollDownAmount;
    }

    public double getOverPayment() {
        return overPayment;
    }

    public void setOverPayment(double overPayment) {
        this.overPayment = overPayment;
    }

    public double getLastDebtPayment() {
        return lastDebtPayment;
    }

    public void setLastDebtPayment(double lastDebtPayment) {
        this.lastDebtPayment = lastDebtPayment;
    }

    public Date getPayOffMonth() {
        return payOffMonth;
    }

    public void setPayOffMonth(Date payOffMonth) {
        this.payOffMonth = payOffMonth;
    }

    public Date getRollDownMonth() {
        return rollDownMonth;
    }

    public void setRollDownMonth(Date rollDownMonth) {
        this.rollDownMonth = rollDownMonth;
    }

    public boolean isRollDown() {
        return rollDown;
    }

    public void setRollDown(boolean rollDown) {
        this.rollDown = rollDown;
    }

    public boolean isFirstDebt() {
        return firstDebt;
    }

    public void setFirstDebt(boolean firstDebt) {
        this.firstDebt = firstDebt;
    }
}